import asyncio
import os
import logging
import re
from datetime import datetime

from fastapi import FastAPI, Request, HTTPException, Response
from pydantic import BaseModel, Field, validator
from fastapi.middleware.cors import CORSMiddleware
from fastapi.staticfiles import StaticFiles
import yaml
import json
from zhipuai import ZhipuAI
import redis
from typing import List, Dict
from logging.handlers import TimedRotatingFileHandler
import concurrent.futures

# 创建 /logs 目录（如果不存在）
if not os.path.exists('logs'):
    os.makedirs('logs')

# 获取当前日期
today = datetime.now().date()
today_str = today.isoformat()

# 配置日志记录
log_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')

# 输出到控制台
console_handler = logging.StreamHandler()
console_handler.setFormatter(log_formatter)

# 按天切割日志文件
file_handler = TimedRotatingFileHandler(
    filename=f'logs/{today_str}.log',
    when='D',  # 按天切割
    interval=1,  # 每天切割一次
    backupCount=30  # 保留最近 30 天的日志文件
)
file_handler.setFormatter(log_formatter)

# 配置根日志记录器
root_logger = logging.getLogger()
root_logger.setLevel(logging.INFO)
root_logger.addHandler(console_handler)
root_logger.addHandler(file_handler)

logger = logging.getLogger(__name__)

# 创建 FastAPI 应用实例
app = FastAPI()

# 定义允许的跨域来源
origins = [
    "*",  # 指定前端域名
]
# 获取配置
REDIS_HOST = os.getenv("REDIS_HOST", "localhost")
REDIS_PORT = os.getenv("REDIS_PORT", 6379)
REDIS_DB = os.getenv("REDIS_DB", 0)
REDIS_PASSWORD = os.getenv("REDIS_PASSWORD", "123456")
ZHIPU_API_KEY = os.getenv("ZHIPU_API_KEY", "9a472ab9d9b740bd8c9816e214d862f9.q8PtGmyzNjm7um25")
ZHIPU_API_KEY_BACKUP = os.getenv("ZHIPU_API_KEY_BACKUP", "9a472ab9d9b740bd8c9816e214d862f9.q8PtGmyzNjm7um25")
UPLOAD_IMG_URL = os.getenv("UPLOAD_IMG_URL", "https://projectflow-test.futurx.cn")
TOKEN = os.getenv("FUTURX_TOKEN", "B5Gf4YzoOlYvdB7")

# 连接 Redis
# r = redis.Redis(host='redis', port=6379, db=0, password=REDIS_PASSWORD)   # 容器间通信
r = redis.Redis(host=REDIS_HOST, port=REDIS_PORT, db=REDIS_DB, password=REDIS_PASSWORD)

# 添加 CORS 中间件
app.add_middleware(
    CORSMiddleware,
    allow_origins=origins,
    allow_credentials=True,
    allow_methods=["*"],
    allow_headers=["*"],
    expose_headers=["*"]  # 暴露所有响应头
)


class ExhibitionInfo(BaseModel):
    date_time: Dict = Field(..., description="日期与对应可用时间段")  # 验证是否存在
    user_input: str = Field(..., description="用户输入信息", min_length=1)
    exhib_list: List = Field(..., description="参展商列表", min_items=1)
    unique_id: str = Field(..., description="唯一标识，用于存储和获取行程规划数据")

    @validator('date_time')
    def validate_time_format(cls, value):
        import re

        # 验证值是否为字典
        if not isinstance(value, dict):
            raise ValueError("date_time 必须是字典类型")

        # 日期格式正则：YYYY-MM-DD
        date_pattern = re.compile(r'^\d{4}-\d{2}-\d{2}$')

        # 时间段格式正则：HH:MM-HH:MM
        time_pattern = re.compile(r'^\d{2}:\d{2}-\d{2}:\d{2}$')

        for date, time_list in value.items():
            # 验证日期格式
            if not date_pattern.match(date):
                raise ValueError(f"日期格式错误: {date}，应符合 YYYY-MM-DD")

            # 验证时间段是否为列表
            if not isinstance(time_list, list):
                raise ValueError(f"日期 {date} 对应的值必须是列表")

            # 验证列表中的每个时间段格式
            for time_str in time_list:
                if not isinstance(time_str, str):
                    raise ValueError(f"时间段必须是字符串: {time_str}")
                if not time_pattern.match(time_str):
                    raise ValueError(f"时间段格式错误: {time_str}，应符合 HH:MM-HH:MM")

                # 额外验证：检查开始时间是否早于结束时间
                start, end = time_str.split('-')
                start_hour, start_min = map(int, start.split(':'))
                end_hour, end_min = map(int, end.split(':'))

                if (start_hour * 60 + start_min) >= (end_hour * 60 + end_min):
                    raise ValueError(f"时间段开始时间必须早于结束时间: {time_str}")

        return value


def parse_llm_json(output_text: str) -> dict:
    """
    从大模型返回的文本中提取并解析JSON数据，具备鲁棒性处理

    Args:
        output_text: 大模型返回的原始文本

    Returns:
        解析后的JSON字典，如果失败则返回空字典
    """
    # 1. 尝试直接解析（如果文本刚好是完整JSON）
    try:
        return json.loads(output_text)
    except json.JSONDecodeError:
        pass

    # 2. 提取JSON代码块（处理被```包围的情况）
    json_match = re.search(r'```json\s*([\s\S]*?)\s*```', output_text)
    if json_match:
        json_str = json_match.group(1)
        try:
            return json.loads(json_str)
        except json.JSONDecodeError as e:
            logger.warning(f"Error parsing extracted JSON: {e}")

    logger.error(f"Failed to parse JSON from LLM output\n{output_text}")
    return {}


def _get_start_time(item):
    """提取行程的开始时间（分钟数），用于排序"""
    time_str = item.get("论坛时间", "").strip()
    if not time_str:
        return float('inf')  # 无时间的行程排在最后

    try:
        # 提取开始时间并转换为分钟数
        start_time = time_str.split('-')[0].strip()
        hour, minute = map(int, start_time.split(':'))
        return hour * 60 + minute
    except (ValueError, IndexError):
        logger.warning(f"无法解析时间格式: {time_str}")
        return float('inf')  # 解析失败的排在最后


def sort_by_forum_time(data):
    """对行程数据进行日期和时间的双重排序"""
    try:
        # 1. 按日期排序
        dates = list(data['value'][0].keys())
        sorted_dates = sorted(dates, key=lambda x: datetime.strptime(x, '%Y-%m-%d'))

        # 创建新的有序字典
        sorted_by_date = {}
        for date in sorted_dates:
            # 2. 对每个日期内的行程按时间排序
            items = data['value'][0][date]
            sorted_items = sorted(items, key=lambda item: _get_start_time(item))
            sorted_by_date[date] = sorted_items

        # 更新原数据结构
        data['value'][0] = sorted_by_date
        return data
    except Exception as e:
        logger.error(f"行程排序出错: {str(e)}")
        return data


def get_response_by_llm(date, time_slots, user_input, exhib_list):
    api_keys = [ZHIPU_API_KEY, ZHIPU_API_KEY_BACKUP]
    for api_key in api_keys:
        try:
            # 提示词读取
            with open("promots/prompts_trip_plan.yaml", "r", encoding="utf-8") as file:
                data = yaml.safe_load(file)

            system_prompt = data.get("system_prompt", "")
            assistant_prompt = data.get("assistant_prompt", "")
            assistant_prompt = assistant_prompt.replace("{{interest}}", user_input).replace("{{exhibition_info}}",
                                                                                            str(exhib_list)).replace(
                "{{date}}", str(date)).replace("{{time}}", str(time_slots))
            # 调用大模型
            client = ZhipuAI(api_key=api_key)
            response = client.chat.completions.create(
                model="glm-4-air-250414",
                messages=[
                    {"role": "system", "content": system_prompt},
                    {"role": "assistant", "content": assistant_prompt},
                    {"role": "user", "content": user_input}
                ],
                stream=False)
            return response.choices[0].message.content
        except Exception as e:
            logger.error(f"使用 API 密钥 {api_key} 调用大模型时出错: {str(e)}")
            if api_key == api_keys[-1]:  # 如果是最后一个密钥，抛出异常
                raise HTTPException(status_code=500, detail=f"调用大模型时出错: {str(e)}")
    return None


async def get_intent_res(user_input, date_time):
    api_keys = [ZHIPU_API_KEY, ZHIPU_API_KEY_BACKUP]
    for api_key in api_keys:
        try:
            with open("promots/prompts_intent.yaml", "r", encoding="utf-8") as f:
                data = yaml.safe_load(f)

            intent_sys_prompt = data.get("intent_sys_prompt", "")
            intent_assistant_prompt = data.get("intent_assistant_prompt", "").replace("{{user_input}}",
                                                                                      user_input).replace("{{time}}",
                                                                                                          json.dumps(
                                                                                                              date_time))

            client = ZhipuAI(api_key=api_key)
            response = await asyncio.to_thread(client.chat.completions.create,
                                               model="glm-4-air-250414",
                                               messages=[
                                                   {"role": "system", "content": intent_sys_prompt},
                                                   {"role": "assistant", "content": intent_assistant_prompt},
                                                   {"role": "user", "content": user_input}
                                               ],
                                               stream=False)
            return response.choices[0].message.content
        except Exception as e:
            logger.error(f"使用 API 密钥 {api_key} 调用大模型时出错: {str(e)}")
            if api_key == api_keys[-1]:
                raise HTTPException(status_code=500, detail=f"调用大模型时出错: {str(e)}")
    return None


async def get_first_schedule_res(date_time, exhib_list, intent_res):
    api_keys = [ZHIPU_API_KEY, ZHIPU_API_KEY_BACKUP]
    for api_key in api_keys:
        try:
            # 提示词读取
            with open("promots/prompts_rearrange.yaml", "r", encoding="utf-8") as file:
                data = yaml.safe_load(file)

            system_prompt = data.get("rearrange_sys_prompt", "")
            assistant_prompt = data.get("rearrange_assistant_prompt", "")
            assistant_prompt = assistant_prompt.replace("{{intent}}", str(intent_res)).replace("{{exhibition_info}}",
                                                                                               str(exhib_list)).replace(
                "{{time_slot}}",
                str(date_time))
            # 调用大模型
            client = ZhipuAI(api_key=api_key)
            response = await asyncio.to_thread(client.chat.completions.create,
                                               model="glm-4-air-250414",
                                               messages=[
                                                   {"role": "system", "content": system_prompt},
                                                   {"role": "assistant", "content": assistant_prompt},
                                                   {"role": "user", "content": "请给出回复"}
                                               ],
                                               stream=False)
            return response.choices[0].message.content
        except Exception as e:
            logger.error(f"使用 API 密钥 {api_key} 调用大模型时出错: {str(e)}")
            if api_key == api_keys[-1]:
                raise HTTPException(status_code=500, detail=f"调用大模型时出错: {str(e)}")
    return None


def merge_exhib_list(merged_data, pre_allocated_data):
    for date, items in pre_allocated_data["value"][0].items():
        if date in merged_data:
            for item in merged_data[date]:
                for exhib in items:
                    if exhib['type'] in ("展商", 'Exhibitor') and exhib not in item["exhib_list"]:
                        item["exhib_list"].append(exhib)
    return merged_data


@app.get("/schedule")
async def get_schedule(request: Request, unique_id: str):
    try:
        # 从 Redis 中读取对应唯一标识的行程数据
        schedule_data = r.get(f'schedule_data:{unique_id}')
        if schedule_data:
            schedule_data = json.loads(schedule_data)['value']
        else:
            schedule_data = []

        # 读取 waic_demo.html 文件内容
        with open('waic_demo.html', 'r', encoding='utf-8') as file:
            html_content = file.read()

        # 生成行程 HTML 内容
        schedule_html = ""
        if not schedule_data:
            logger.error("当前唯一标识没有找到路径规划数据，请检查标识是否正确～")
            raise HTTPException(status_code=500, detail=f"当前唯一标识没有找到路径规划数据，请检查标识是否正确～")
        logger.info("schedule_data: %s", schedule_data[0])
        for schedule_key in schedule_data[0].keys():
            schedule_value = schedule_data[0][schedule_key]
            schedule_html += f""" <div class="day-title">{schedule_key}</div> """
            for item in schedule_value:
                type_html = ""
                if item.get('type', "展商") in ('论坛', 'Forum'):
                    type_html = f"""
                    <div class="event-info">
                        <img src="file/static/倒计时－美店.png">
                        <p data-lang-key="type">论坛</p>
                        <br>
                        <span style="font-size: 16px;">{item.get('论坛名称', "")}</span>
                    </div>
                    """
                elif item.get('type', "展商") in ('展商', 'Exhibitor'):
                    type_html = f"""
                    <div class="event-info">
                        <img src="file/static/hugeicons_store-02.png">
                        <span style="vertical-align: middle;">{item.get('企业名称', "")}</span>
                    </div>
                    """
                schedule_item = f"""
                <div class="event-item">
                    <div class="event-time-icon">
                        <i class="fas fa-clock"></i>
                    </div>
                    <div>
                        <div class="event-time">{item.get('论坛时间', "")}</div>
                        <div class="location">{item.get('tip', "")}</div>
                        {type_html}
                        <i class="fas fa-map-marker-alt icon-16" style="font-size: 12px;"></i>
                        <div class="location inline">{item.get('论坛地址', "")}</div>
                    </div>
                </div>
                """
                schedule_html += schedule_item

        # 将行程 HTML 内容插入到 waic_demo.html 中
        html_content = html_content.replace('{{ flowUrl }}', UPLOAD_IMG_URL)
        html_content = html_content.replace('{{ token }}', TOKEN)
        html_content = html_content.replace('<!-- 行程内容将动态插入这里 -->', schedule_html)

        return Response(content=html_content, media_type="text/html")
    except Exception as e:
        logger.error("读取 Redis 数据时出错: %s", str(e))
        raise HTTPException(status_code=500, detail=f"读取 Redis 数据时出错: {str(e)}")


@app.post("/schedule_plan")
async def upload_exhibition_info(request: Request, exhibition_info: ExhibitionInfo):
    logger.info("=============>开始路径规划<================")
    unique_id = exhibition_info.unique_id
    date_time = exhibition_info.date_time
    exhib_list = exhibition_info.exhib_list
    user_input = exhibition_info.user_input

    try:
        # 1.用户意图识别
        intent_res = await get_intent_res(user_input, date_time)
        intent_res = parse_llm_json(intent_res)
        logger.info("用户意图识别结果: %s", intent_res)
        if not intent_res:
            logger.error("用户意图识别失败")
            intent_res = user_input  # 用户意图识别失败，直接进行下一步预分配

        # 2.展商预分配
        pre_allocated_data = await get_first_schedule_res(date_time, exhib_list, intent_res)
        pre_allocated_data = parse_llm_json(pre_allocated_data.replace("'", '"'))
        if not pre_allocated_data:
            logger.error("展商预分配失败")
            raise HTTPException(status_code=500, detail="展商预分配失败")
        logger.info("展商预分配结果: %s", pre_allocated_data)

        # 3.合并逻辑
        merged_data = {}
        for date in intent_res:
            if date in date_time:
                # 确保用户输入和时间段数量一致
                user_inputs = intent_res[date]
                slots = date_time[date]

                # 处理数量不匹配的情况
                if len(user_inputs) > len(slots):
                    # 意图识别的长度大于可用时间的长度，拼接意图
                    combined_input = " ".join(user_inputs)
                    user_inputs = [combined_input]
                elif len(slots) > len(user_inputs):
                    # 可用时间长度大于意图识别长度，复制意图
                    user_inputs = [user_inputs[0]] * len(slots)

                items = []

                for user_input, slot in zip(user_inputs, slots):
                    item = {
                        "user_input": user_input,
                        "time_slots": slot
                    }
                    # 添加当天可规划的行程
                    available_exhib = []
                    for exhib in exhib_list:
                        exhib_date = exhib.get("论坛日期")
                        exhib_time = exhib.get("论坛时间")
                        # fixme 展商合并的时候不规划
                        # if not exhib_date:
                        #     available_exhib.append(exhib)
                        # 论坛规划
                        if exhib_date == date:
                            if exhib_time:
                                start_exhib, end_exhib = map(str.strip, exhib_time.split('-'))
                                start_slot, end_slot = map(str.strip, item["time_slots"].split('-'))
                                # if start_exhib >= start_slot and end_exhib <= end_slot:
                                if start_exhib >= start_slot:
                                    available_exhib.append(exhib)
                    item["exhib_list"] = available_exhib
                    items.append(item)

                merged_data[date] = items
        if not merged_data:
            logger.error("没有匹配的行程")
            raise HTTPException(status_code=400, detail="可用时间没有匹配的行程，去别处看看吧～")
        logger.info("合并前的数据: %s", str(merged_data))

        # 合并预分配展商和合并之后的数据
        merged_result = merge_exhib_list(merged_data, pre_allocated_data)
        logger.info("合并后的数据: %s", str(merged_result))

        # 4.整体路径规划 - 多线程执行
        all_schedule_plan = None
        tasks = []
        with concurrent.futures.ThreadPoolExecutor() as executor:
            for date, value_list in merged_data.items():
                for item in value_list:
                    time_slots = item["time_slots"]
                    user_input = item["user_input"]
                    exhib_list = item["exhib_list"]
                    task = executor.submit(get_response_by_llm, date, time_slots, user_input, exhib_list)
                    tasks.append(task)

            for task in concurrent.futures.as_completed(tasks):
                try:
                    llm_resp = task.result()
                    logger.info(f"llm路径规划结果: {llm_resp}")
                    llm_resp = parse_llm_json(llm_resp)  # 解析失败之内返回空，避免影响其他行程
                    if not all_schedule_plan:
                        all_schedule_plan = llm_resp
                    else:
                        date_key = list(llm_resp['value'][0].keys())[0]
                        if date_key in all_schedule_plan['value'][0]:
                            flattened_list = []
                            for item in llm_resp['value'][0][date_key]:
                                if isinstance(item, list):
                                    flattened_list.extend(item)
                                else:
                                    flattened_list.append(item)
                            all_schedule_plan['value'][0][date_key].extend(flattened_list)
                        else:
                            all_schedule_plan['value'][0].update(llm_resp['value'][0])
                except Exception as e:
                    logger.error(f"处理任务时出错: {str(e)}")
                    raise HTTPException(status_code=500, detail=f"处理任务时出错: {str(e)}")

        logger.info("all_schedule_plan%s", str(all_schedule_plan))

        # 5.按论坛时间为行程排序
        resp_data = sort_by_forum_time(all_schedule_plan)

        # 6.将行程数据存储到 Redis 中，使用唯一标识
        r.set(f'schedule_data:{unique_id}', json.dumps(resp_data))
        logger.info(f"唯一标识：{unique_id}模型规划信息已保存到redis")
        return resp_data
    except json.JSONDecodeError:
        logger.error("llm返回的结果不是有效的 JSON 格式")
        raise HTTPException(status_code=500, detail="大模型返回的结果不是有效的 JSON 格式")
    except HTTPException:
        raise
    except Exception as e:
        logger.error("处理请求时出错: %s", str(e))
        raise HTTPException(status_code=500, detail=f"处理请求时出错: {str(e)}")


# 挂载静态文件目录
app.mount("/file", StaticFiles(directory=".", html=False), name="static")
# 挂载 logs 目录
app.mount("/logs", StaticFiles(directory="./logs"), name="logs")
